webui/src/app/namespaces/[namespace]/clusters/[cluster]/page.tsx (144 lines of code) (raw):
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
"use client";
import { Box, Container, Typography, Chip, Badge } from "@mui/material";
import { ClusterSidebar } from "../../../../ui/sidebar";
import { useState, useEffect } from "react";
import { listShards } from "@/app/lib/api";
import { AddShardCard, ResourceCard } from "@/app/ui/createCard";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { LoadingSpinner } from "@/app/ui/loadingSpinner";
import DnsIcon from "@mui/icons-material/Dns";
import StorageIcon from "@mui/icons-material/Storage";
import EmptyState from "@/app/ui/emptyState";
export default function Cluster({ params }: { params: { namespace: string; cluster: string } }) {
const { namespace, cluster } = params;
const [shardsData, setShardsData] = useState<any[]>([]);
const [loading, setLoading] = useState<boolean>(true);
const router = useRouter();
useEffect(() => {
const fetchData = async () => {
try {
const fetchedShards = await listShards(namespace, cluster);
if (!fetchedShards) {
console.error(`Shards not found`);
router.push("/404");
return;
}
setShardsData(fetchedShards);
} catch (error) {
console.error("Error fetching shards:", error);
} finally {
setLoading(false);
}
};
fetchData();
}, [namespace, cluster, router]);
if (loading) {
return <LoadingSpinner />;
}
const formatSlotRanges = (ranges: string[]) => {
if (!ranges || ranges.length === 0) return "None";
if (ranges.length <= 2) return ranges.join(", ");
return `${ranges[0]}, ${ranges[1]}, ... (+${ranges.length - 2} more)`;
};
return (
<div className="flex h-full">
<ClusterSidebar namespace={namespace} />
<div className="flex-1 overflow-auto">
<Box className="container-inner">
<Box className="mb-6 flex items-center justify-between">
<div>
<Typography
variant="h5"
className="flex items-center font-medium text-gray-800 dark:text-gray-100"
>
<StorageIcon className="mr-2 text-primary dark:text-primary-light" />
{cluster}
<Chip
label={`${shardsData.length} shards`}
size="small"
color="primary"
className="ml-3"
/>
</Typography>
<Typography
variant="body2"
className="mt-1 text-gray-500 dark:text-gray-400"
>
Cluster in namespace: {namespace}
</Typography>
</div>
</Box>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
<Box className="col-span-1">
<AddShardCard namespace={namespace} cluster={cluster} />
</Box>
{shardsData.length > 0 ? (
shardsData.map((shard, index) => (
<Link
key={index}
href={`/namespaces/${namespace}/clusters/${cluster}/shards/${index}`}
className="col-span-1"
>
<ResourceCard
title={`Shard ${index + 1}`}
tags={[
{
label: `${shard.nodes.length} nodes`,
color: "secondary",
},
...(shard.migrating_slot >= 0
? [{ label: "Migrating", color: "warning" }]
: []),
]}
>
<div className="space-y-2 text-sm">
<div className="flex justify-between">
<span className="text-gray-500 dark:text-gray-400">
Slots:
</span>
<span className="font-medium">
{formatSlotRanges(shard.slot_ranges)}
</span>
</div>
{shard.target_shard_index >= 0 && (
<div className="flex justify-between">
<span className="text-gray-500 dark:text-gray-400">
Target Shard:
</span>
<span className="font-medium">
{shard.target_shard_index + 1}
</span>
</div>
)}
{shard.migrating_slot >= 0 && (
<div className="flex justify-between">
<span className="text-gray-500 dark:text-gray-400">
Migrating Slot:
</span>
<Badge color="warning" variant="dot">
<span className="font-medium">
{shard.migrating_slot}
</span>
</Badge>
</div>
)}
</div>
</ResourceCard>
</Link>
))
) : (
<Box className="col-span-full">
<EmptyState
title="No shards found"
description="Create a shard to get started"
icon={<DnsIcon sx={{ fontSize: 60 }} />}
/>
</Box>
)}
</div>
</Box>
</div>
</div>
);
}